package com.linkgie.galaxyframework.crypto;

import java.util.Base64;

import com.linkgie.galaxyframework.utils.BinaryUtils;
import com.linkgie.galaxyframework.utils.RandomUtils;

/**
 * 基于口令的对称加解密的工具类；
 * <p>
 * 提供更简单的对文本消息的加解密操作;
 * 注：
 * PBE : Password-Based Encryption；
 */
public class PBECipher {

    /**
     * 密文编码的前缀；
     * <p>
     * 作为 Base64 编码的密文消息的前缀，用于标识此消息已经加密；
     * <p>
     * 另外，有意选择与 Base64 不兼容的字符 !@ 加入前缀，避免与 Base64 编码的密文混淆，避免被错误地一同解码；
     */
    private static final String CIPHERTEXT_PREFIX = "!@";

    private final SymmetricCipherService symmetricCipherService;

    private final String password;

    public static PBECipher createAesCipher(String password) {
        return createCipher(password, "AES");
    }

    public static PBECipher createSm4Cipher(String password) {
        return createCipher(password, "SM4");
    }

    public static PBECipher createCipher(String password, String algorithm) {
        SymmetricCipherService symmetricCipherService = Crypto.getSymmetricCipherService(algorithm);
        return new PBECipher(password, symmetricCipherService);
    }

    public PBECipher(String password, SymmetricCipherService symmetricCipherService) {
        this.password = password;
        this.symmetricCipherService = symmetricCipherService;
    }

    /**
     * 加密指定的关键信息；
     * <p>
     * 如果指定的消息已经是加密消息，则直接返回；
     * 
     * @param plaintext 明文；
     * @param password  口令；
     * @return
     */
    public String encrypt(String plaintext) {
        return encrypt(plaintext, RandomUtils.generateLong());
    }

    /**
     * 加密指定的关键信息；
     * <p>
     * 如果指定的消息已经是加密消息，则直接返回；
     * 
     * @param plaintext 明文；
     * @param password  口令；
     * @param nonce     随机数；
     * @return
     */
    public String encrypt(String plaintext, long nonce) {
        return encrypt(plaintext, nonce, "UTF-8");
    }

    /**
     * 加密指定的关键信息；
     * <p>
     * 如果指定的消息已经是加密消息，则直接返回；
     * 
     * @param plaintext 明文；
     * @param password  口令；
     * @param nonce     随机数；
     * @return
     */
    public String encrypt(String plaintext, long nonce, String charset) {
        if (plaintext == null) {
            return null;
        }
        // 当存在加密消息前缀时，认为此消息已经是加密消息，直接返回；
        if (plaintext.startsWith(CIPHERTEXT_PREFIX)) {
            return plaintext;
        }
        // 生成加密密钥；
        byte[] seed = generateSeed(password, nonce);
        SymmetricKey cipherKey = symmetricCipherService.generateKey(seed);

        // 编码明文；
        byte[] plainbytes = BinaryUtils.toBytes(plaintext, charset);
        // 加密信息；
        byte[] cipherbytes = symmetricCipherService.encrypt(cipherKey, plainbytes);
        // 拼接随机数前缀；
        byte[] cipherbytesWithNonce = new byte[cipherbytes.length + 8];
        BinaryUtils.toBytes(nonce, cipherbytesWithNonce, 0);
        System.arraycopy(cipherbytes, 0, cipherbytesWithNonce, 8, cipherbytes.length);

        // 对密文消息进行编码；
        // 加入消息前缀，标识此消息已加密；
        return CIPHERTEXT_PREFIX + Base64.getEncoder().encodeToString(cipherbytesWithNonce);
    }

    /**
     * 解密指定的消息；
     * <p>
     * 如果指定的消息不是加密消息，则直接返回；
     * 
     * @param ciphertext
     * @param password
     * @return
     */
    public String decrypt(String ciphertext) throws DecryptionException {
        return decrypt(ciphertext, "UTF-8");
    }
    /**
     * 解密指定的消息；
     * <p>
     * 如果指定的消息不是加密消息，则直接返回；
     * 
     * @param ciphertext
     * @param password
     * @return
     */
    public String decrypt(String ciphertext, String charset) throws DecryptionException {
        if (ciphertext == null) {
            return null;
        }
        // 缺少加密消息前缀时，认为此消息不是加密消息，直接返回；
        if (!ciphertext.startsWith(CIPHERTEXT_PREFIX)) {
            return ciphertext;
        }
        // 去掉消息前缀；
        ciphertext = ciphertext.substring(CIPHERTEXT_PREFIX.length());
        // 解析随机数；
        byte[] cipherbytesWithNonce = Base64.getDecoder().decode(ciphertext);
        long nonce = BinaryUtils.toLong(cipherbytesWithNonce, 0);
        // 生成加密密钥；
        byte[] seed = generateSeed(password, nonce);
        SymmetricKey cipherKey = symmetricCipherService.generateKey(seed);
        // 解密信息；
        byte[] plainbytes = symmetricCipherService.decrypt(cipherKey, cipherbytesWithNonce, 8,
                cipherbytesWithNonce.length - 8);
        // 解码明文；
        return BinaryUtils.toString(plainbytes, charset);
    }

    /**
     * 拼接随机数和字段名，生成加密密钥的种子；
     * <p>
     * 注：返回值中，随机数的字节长度为 8 字节，排列在前；
     * 
     * @param field
     * @param random
     * @return
     */
    private static byte[] generateSeed(String field, long random) {
        byte[] fieldBytes = BinaryUtils.toBytes_UTF8(field);
        byte[] seed = new byte[fieldBytes.length + 8];
        BinaryUtils.toBytes(random, seed, 0);
        System.arraycopy(fieldBytes, 0, seed, 8, fieldBytes.length);
        return seed;
    }

}
